package com.sweet.logging.storage.query;

import com.sweet.logging.storage.SearchService;
import com.sweet.logging.storage.store.SimpleFileStorageService;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;

/**
 * 日志查询服务
 *
 *    初始化时，读取索引文件，建立内存索引树。索引树需要定时刷新，否则查不到最新的数据。
 *
 *
 * @author xiexiaofeng
 * @version 1.0     2017年09月09日  20点41分
 */
public class SimpleFileStorageQueryService implements SearchService
{

    private String logRootDir;
    private TreeMap<Long, Long> indexTree = new TreeMap<>(); // 索引
    private long lastIndexLoadPostion;
    private Timer timer; // 定时加载最新的索引

    public SimpleFileStorageQueryService(String logRootDir) throws IOException
    {
        this.logRootDir = logRootDir;
        timer = new Timer();
        timer.schedule(new TimerTask()
        {
            public void run() {
                try
                {
                    loadIndex();
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }, 0 , 10000);
    }

    /**
     * 索引建设中，不允许查询
     * @throws IOException 文件不可读，读文件错误等
     */
    private synchronized void loadIndex() throws IOException
    {
        // 加载内存索引
        FileChannel channel = new RandomAccessFile(this.logRootDir +
                SimpleFileStorageService.INDEX_NAME,"r").getChannel();
        channel.position(lastIndexLoadPostion);
        ByteBuffer  buffer  = ByteBuffer.allocate(1600); // 一次读一百条索引
        int size;
        while ((size = channel.read(buffer)) > 0)
        {
            lastIndexLoadPostion += size;
            buffer.flip();
            for (int i = 0; i < size / 16; i++)
            {
                indexTree.put(buffer.getLong(), buffer.getLong());
            }
            buffer.compact();
        }
        channel.close();
    }

    /**
     *    先用时间戳索引缩小查询范围，在根据查询条件进行过滤
     * 返回结果集暂时限定一千行，防止过大的结果集，影响内
     * 存性能和网络效率。
     *
     * @param start 开始时间
     * @param end 结束时间
     * @param condition 按条件，使用关键字查询
     * @param keyWord 查询关键字
     * @return 结果集
     * @throws IOException 读数据文件错误
     */
    @Override
    public synchronized StringBuilder search(long start, long end, String condition, String keyWord)
            throws IOException
    {
        end += 999L; // 这个时间不带毫秒的，加上一个最大毫秒数，否则查不出当前这一秒的数据
        if (start > indexTree.lastKey())
            return new StringBuilder("not find");
        Long startPosition;
        Long endPosition;
        try
        {
             startPosition = indexTree.get(indexTree.higherKey(start)); // 数据起始位置
             endPosition   = indexTree.get(indexTree.lowerKey(end));  // 数据终止位置
        } catch (Exception e) {
            return new StringBuilder("not find");
        }
        boolean usingHash  = ECondition.EQUAL.getCode().equals(condition) || ECondition.NOTEQUAL.getCode().equals(condition);
        int  hash          = keyWord.trim().hashCode();
        StringBuilder result = new StringBuilder();
        RandomAccessFile dataFile = null;
        LinkedList<Long> positionPointers = null;
        if (usingHash)
        {
            positionPointers = new LinkedList<>(); // hash查询返回的结果集
            hash(positionPointers, hash, startPosition, endPosition, ECondition.EQUAL.getCode().equals(condition));
            if (positionPointers.size() == 0)
                return new StringBuilder("not find by using hash index");
        }
        try
        {
            dataFile = new RandomAccessFile(this.logRootDir + SimpleFileStorageService.DATA_FILE_NAME, "r");
            String str;
            dataFile.seek(positionPointers != null ? positionPointers.pollFirst() : startPosition); //将文件流的位置移动到pos字节处
            Stack<String> stack = new Stack<>(); // 用于倒序输出
            while((str = dataFile.readLine()) != null && dataFile.getFilePointer() < endPosition)
            {
                if (ECondition.LIKE.getCode().equals(condition))
                {
                    if (str.contains(keyWord))
                        stack.push(str.endsWith("#sweet_end#") ? str.substring(0, str.length() - 11) : str);
                } else
                {
                    stack.push(str.endsWith("#sweet_end#") ? str.substring(0, str.length() - 11) : str);
                    if (usingHash && str.endsWith("#sweet_end#"))
                    {
                        if (positionPointers.size() > 0)
                        {
                            dataFile.seek(positionPointers.removeFirst()); // 跳到下一个数据点
                            continue;
                        } else
                            break;
                    }
                }
                if (stack.size() >= 1000) // 防止返回数据过多
                    break;
            }
            while(!stack.empty())
            {
                String logPoped = stack.pop();
                result.append(logPoped).append("<br>");
            }
        } catch (IOException e)
        {
            e.printStackTrace();
        } finally
        {
            if (dataFile != null)
                dataFile.close();
        }
        return result;
    }

    /**
     * 做一次hash全数据扫描
     * @param positionPointers 哈希查询出来的集合
     * @param hash 哈希值
     * @param startPosition 依据时间区间查询到的数据起始位置
     * @param endPosition 依据时间区间查询到的数据终止位置
     * @param equal 是否相等
     * @throws IOException 读取hash索引文件异常
     */
    private void hash(LinkedList<Long> positionPointers, int hash, Long startPosition, Long endPosition, boolean equal) throws IOException
    {
        // 加载内存索引
        FileChannel channel = new RandomAccessFile(this.logRootDir +
                SimpleFileStorageService.INDEX_HASH_NAME,"r").getChannel();
        ByteBuffer  buffer  = ByteBuffer.allocate(1200); // 一次读一百条索引
        int size;
        while ((size = channel.read(buffer)) > 0)
        {
            buffer.flip();
            for (int i = 0; i < size / 12; i++)
            {
                int  curHash     = buffer.getInt();
                long curPosition = buffer.getLong();
                if (equal == (curHash == hash) && startPosition <= curPosition && curPosition < endPosition)
                    positionPointers.add(curPosition);
                if (positionPointers.size() >= 1000)
                    break; // 防止返回数据集合过大
            }
            buffer.compact();
        }
        channel.close();
    }

}
